use std::fs::{self, File, OpenOptions};
use std::io::Write;
use std::os;
use std::path::{Path, PathBuf};

use memmap2::{MmapMut, MmapOptions};

use crate::record;

pub struct FileMmapAppender {
    file_name: Box<Path>,
    last_roll_hour: u32,
    file: Option<File>,
    file_size: u64,
    pos: usize,
    mmap_size: u64,
    mmap: Option<MmapMut>,
}

impl FileMmapAppender {
    pub fn new<P: AsRef<Path>>(file_name: P, mut mmap_size: u64) -> crate::Result<Self> {
        Ok(Self {
            file_name: {
                let path: Box<Path> = Box::from(file_name.as_ref());
                {
                    let log_dir = path.parent().ok_or("parent is None")?;
                    if !log_dir.exists() {
                        fs::create_dir_all(log_dir)?
                    }
                }
                path
            },
            last_roll_hour: 0,
            file: None,
            file_size: 0,
            pos: 0,
            mmap_size: {
                let page_size = page_size::get() as u64;
                if mmap_size % page_size != 0 {
                    mmap_size = (mmap_size / page_size) * page_size;
                }
                if mmap_size == 0 {
                    mmap_size = page_size;
                }
                mmap_size
            },
            mmap: None,
        })
    }

    pub fn build(self) -> super::Appender {
        super::Appender::FileMmapAppender(self)
    }

    pub fn name(&self) -> &str {
        "file_mmap_appender"
    }

    #[cfg(unix)]
    fn symlink_file<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> crate::Result<()> {
        os::unix::fs::symlink(src, dst)?;
        Ok(())
    }
    #[cfg(windows)]
    fn symlink_file<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> crate::Result<()> {
        os::windows::fs::symlink_file(src, dst)?;
        Ok(())
    }

    #[cfg(unix)]
    fn gen_file_name(&self, time: &chrono::DateTime<chrono::Local>) -> Option<PathBuf> {
        Some(PathBuf::from(format!(
            "{}.{}",
            self.file_name.to_str()?,
            time.format("%Y%m%d-%H")
        )))
    }

    #[cfg(windows)]
    fn gen_file_name(&self, time: &chrono::DateTime<chrono::Local>) -> Option<PathBuf> {
        let parent = self.file_name.parent()?.to_str()?;
        let file_stem = self.file_name.file_stem()?.to_str()?;
        let log_extension = self.file_name.extension()?.to_str()?;
        Some(PathBuf::from(format!(
            "{}/{}.{}.{}",
            parent,
            file_stem,
            time.format("%Y%m%d-%H"),
            log_extension
        )))
    }

    fn try_remove_mmap_placeholder(file: &File) -> crate::Result<()> {
        let file_size = file.metadata()?.len();
        if file_size != 0 {
            let mmap = unsafe { MmapOptions::new().map(&file)? };
            if mmap.last() == Some(&0) {
                if let Some(pos) = mmap.iter().rposition(|&c| c != 0) {
                    file.set_len(pos as u64 + 1)?;
                }
            }
        }
        Ok(())
    }

    fn make_mmap(&mut self) -> crate::Result<()> {
        if let Some(ref file) = self.file {
            self.pos = 0;
            file.set_len(self.file_size + self.mmap_size)?;
            let mmap = unsafe {
                MmapOptions::new()
                    .populate()
                    .offset(self.file_size)
                    .len(self.mmap_size as _)
                    .map_mut(&file)?
            };
            self.mmap = Some(mmap);
        } else {
            return Err("file is None".into());
        }
        Ok(())
    }

    fn create_file(&mut self, time: &chrono::DateTime<chrono::Local>) -> crate::Result<()> {
        let formated_file_name = self.gen_file_name(time).ok_or("gen_file_name failed")?;

        let file = OpenOptions::new()
            .read(true)
            .write(true)
            .append(true)
            .create(true)
            .open(&formated_file_name)?;

        drop(fs::remove_file(&self.file_name));
        if !cfg!(windows) {
            //no symlink in windows because need permissions
            Self::symlink_file(
                &formated_file_name
                    .file_name()
                    .ok_or("formated_file_name is None")?,
                &self.file_name,
            )?;
        }

        Self::try_remove_mmap_placeholder(&file)?;
        self.file_size = file.metadata()?.len();
        self.file = Some(file);

        self.make_mmap()?;

        Ok(())
    }

    pub fn process(&mut self, record: &record::Record) -> crate::Result<()> {
        let cur_secs = record.time.timestamp();
        let cur_hours = (cur_secs / 3600) as u32;
        if cur_hours > self.last_roll_hour {
            if let Some(ref file) = self.file {
                let _ = file.set_len(self.file_size + self.pos as u64);
            }

            self.create_file(&record.time)?;
            self.last_roll_hour = cur_hours;
        }
        if self.pos as u64 + record.content.len() as u64 > self.mmap_size {
            self.file_size = self.file_size + self.pos as u64;
            self.make_mmap()?;
        }
        if let Some(ref mut mmap) = self.mmap {
            let mut writer = &mut mmap[self.pos..];
            writer.write_all(record.content.as_bytes())?;
            self.pos += record.content.len();
        } else {
            return Err("mmap is None".into());
        }
        Ok(())
    }
}

impl PartialEq for FileMmapAppender {
    fn eq(&self, _other: &Self) -> bool {
        true
    }
}

impl Drop for FileMmapAppender {
    fn drop(&mut self) {
        if let Some(ref file) = self.file {
            let _ = file.set_len(self.file_size + self.pos as u64);
        }
    }
}
